Skip to content

refactor(plugin-chatbot): drop the inert blanket cast on useChat's options object - #8401

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-8378-dead-usechat-cast
Sep 7, 2026
Merged

refactor(plugin-chatbot): drop the inert blanket cast on useChat's options object#8401
os-justin merged 2 commits into
mainfrom
claude/issue-8378-dead-usechat-cast

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #8378

Removes useChat({...} as any) in packages/plugin-chatbot/src/useObjectChat.ts, and records in a comment what the measurement actually found.

Spelling note: generic type arguments are written as WORDS where they appear, because GitHub's body sanitizer eats tag-shaped fragments.

The site, re-derived

The brief said the file had moved twice and not to trust :735. Re-derived by content, not by line number: grep -n 'as any' returns four hits, of which one (:368) is a doc comment naming the cast objectui#8342 removed and one (:808) is an unrelated metadata read. The site is packages/plugin-chatbot/src/useObjectChat.ts:735, the line } as any);, unique in the file.

⚠️ Contradicts the brief: :735 was still correct. PR #8377 landed above the site but the intervening edits were net line-neutral at this point in the file. I re-derived it independently rather than trusting it, and the base read below confirms the line number as well as the content — but the dispatch's expectation that the number had moved was wrong.

Base side read with a literal sha (fadf6cd7317e1ac8a011f4e6b986187223c7fdba), interrogated in both directions so it cannot be an index read of my own work:

question the base copy must answer answer
contains } as any); yes, printed at line 735
contains SdkChatMessage (PR #8377 landed marker) 6
contains this PR's new comment token 0
branch HEAD contains that comment token 1
branch HEAD contains } as any); 0
a token present in neither (negative control) 0

Base blob ff06f46f94c81cda552f741484e36e36c5df08fe, branch blob 0c3c0da6259ba6725ccb6540a1185428d16a0372.

⚠️ This is a narrowing, not an elimination — read this before approving

The card asked whether the outer cast is inert on its own merits, or only because the nested (aiInitialMessages as any) absorbs the mismatch. Measured as a 2x2, each leg mutated on disk (hash proven to differ from the HEAD blob), each restored by state:

outer cast nested cast tsc --noEmit tsc -p tsconfig.test.json
present present (base) exit 0, 0 diagnostics exit 0, 0 diagnostics
removed present (this PR) exit 0, 0 diagnostics exit 0, 0 diagnostics
present removed exit 0, 0 diagnostics exit 0, 0 diagnostics
removed removed exit 2, TS2322 exit 2, TS2322

The bottom row is the answer:

src/useObjectChat.ts(704,5): error TS2322: Type '{ id: string; role: "user" | "assistant" | "system";
parts: Record<string, unknown>[]; }[] | undefined' is not assignable to type
'UIMessage<unknown, UIDataTypes, UITools>[] | undefined'.
  ... Type 'Record<string, unknown>' is not assignable to type 'UIMessagePart<UIDataTypes, UITools>'.

So the outer cast is inert only because the nested one is absorbing a real mismatch. The suppression count on this call goes 2 to 1, not 2 to 0. The card's headline — "this cast is DEAD" — is true only conditionally, and the condition is that the nested cast stays. Row 3 shows the converse: with the nested cast gone the outer one would have been load-bearing.

It is still worth landing, for a reason that is about scope rather than count. The outer cast widened the entire options argument to any: transport, onError, excess-property checking and the generic instantiation all went unchecked. The nested cast suppresses exactly one property. Removing the outer restores compiler checking of everything except messages. That is real checking newly acquired, not zero — but it is a different claim from the one the card makes, and the card should be re-read in that light.

Which instantiation results

The card asked this explicitly. Probed with the TypeScript compiler API against the real program (no mutation), before and after:

resolved chatResult type argument type
with the cast UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>> any
without the cast UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>> { transport: DefaultChatTransport<UIMessage<unknown, UIDataTypes, UITools>> or undefined; messages: any; onError: ((err: Error) => void) or undefined; }

Identical, in both the main and the test program. Inference does start running, and it lands on exactly the type the default already supplied, so nothing downstream of chatResult moves.

The declaration-level argument — and where it stops

useChat is declared useChat<UI_MESSAGE extends UIMessage = UIMessage>(options?: UseChatOptions<UI_MESSAGE>): UseChatHelpers<UI_MESSAGE>, and UseChatOptions is ({ chat: Chat<UI_MESSAGE> } | ChatInit<UI_MESSAGE>) & { throttle?; experimental_throttle?; resume? }. Only three properties are passed, and each contributes to UI_MESSAGE inference as follows:

  • onError is declared ChatOnErrorCallback, and that is type ChatOnErrorCallback = (error: Error) => voidno type parameter occurs in it at all. This one is genuinely declaration-level and permanent: whatever is passed, this property can never produce an inference candidate for UI_MESSAGE.
  • messages is any at the call, and an any source contributes no inference candidate (it carries none of the object/intersection flags the inference walk requires). Declaration-level as a rule of the algorithm, but contingent on the nested cast staying.
  • transport is therefore the sole inference source, and it is DefaultChatTransport<UIMessage<unknown, UIDataTypes, UITools>> — itself pinned there because new DefaultChatTransport({ ... }) at :637 supplies no explicit type argument and no candidate either, leaving its own UI_MESSAGE to fall back to its constraint.

I cannot make the strong claim. "This cast is a no-op for every possible instantiation" is not true here, and saying it would be dressing the result up. The honest claim is the weaker one the brief asks for:

The cast is inert today, and precisely what would make it load-bearing is: deleting the nested (aiInitialMessages as any) without first fixing the parts builder. That is not hypothetical — it is row 4 of the table above, measured red in both programs. Secondarily, an excess or misspelled property on the options object, or a transport narrowed to a custom UI_MESSAGE, would also be caught now and were hidden before.

That weakness is why the deletion ships with a comment rather than silently: the next reader who removes the nested cast expecting the last suppression to fall away needs to know what they will hit, and needs to not "fix" it by re-widening the call.

Instrument discipline

Both projects, with membership proof. type-check for this package is tsc --noEmit && tsc -p tsconfig.test.json; both were run separately for every leg. tsconfig.json excludes tests by directory, and tsconfig.test.json includes only *.test.ts(x), so membership was not assumed:

  • main program --listFiles | grep -c 'src/useObjectChat\.ts$' = 1; lit control (a made-up filename on the same instrument) = 0
  • test program, same two readings = 1 and 0 (pulled in transitively by six useObjectChat.* suites, named in the run)

The instrument is not merely present but demonstrably able to fail: row 4 turned both programs red on this exact file.

Restore-integrity / instrument-liveness control. Re-inserting the cast onto the implementation returns both projects to exit 0. Reported as what it is — evidence the tree is unbroken and the harness restores correctly — not as a second reading about the cast, since it restores code main keeps green by construction.

Dependents, downstream direction. pnpm --workspace-concurrency=2 --filter '...@object-ui/plugin-chatbot' type-check, after building the full closure first so a stale dist could not masquerade as a verdict (the TS2307/TS2882 signature objectui#8342 hit).

Scope: 7 of 47 workspace projects

Zero error TS across the run; per package: packages/plugin-chatbot Done, packages/app-shell Done, apps/console Done, apps/site Done, examples/schema-catalog Done, examples/console-starter Done, examples/byo-backend-console Done.

Vitest. Run from the repo root as AGENTS.md requires (pnpm exec vitest run packages/plugin-chatbot/), which reports RUN v4.1.10 /home/user/objectui-issue-8378 — the repo root, not a package dir. 40 test files passed, 474 tests passed. vitest list names all six useObjectChat.* suites and shows zero apps/console files, so this is not the objectui#3378 false green. As the brief notes, vitest transpiles types away and can never see a type-level change; it is here to show nothing moved.

Lint is the package-scoped eslint .: exit 0, 88 problems (0 errors, 88 warnings). Against the base copy of the same file on the same instrument: 89 problems (0 errors, 89 warnings), with no-explicit-any hits going 13 to 12. The deleted cast was a real reported any, and exactly one warning went away.

Mutation hygiene. Every leg: absolute-path trap ... EXIT INT TERM, mutation proved on disk by git hash-object differing from git rev-parse HEAD:PATH (empty hash guarded as failure), anchor counted in both directions (deleted text 1 to 0, injected text 1 to 2), restore verified by state — hash equal again and git diff HEAD empty — never by an exit code. The ablation legs ran from a committed implementation.

Changeset

node scripts/check-changeset-presence.mjs decided it, and its verdict line is:

✅  1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s):
    .changeset/issue-8378-usechat-options-cast.md.
    Every one of them has an EMPTY frontmatter — declared as releasing nothing, which
    is the explicit exemption and a complete answer to this gate.

Empty frontmatter, argued the way PR #8389 argued it — from a measurement, not by feel. Building @object-ui/plugin-chatbot from this branch and from the merge-base produced a byte-identical dist across all 34 published artifacts (.d.ts, .js, .cjs, .css, compared by sha256sum manifest).

That "identical" verdict was itself proved live, because a comparison that can only ever say "identical" is not a measurement: a control mutation to an emitted string literal in the same file, verified to have reached dist/index.js before the comparison ran, made the same manifest report differing hashes for index.js and index.umd.cjs. A first control attempt — an unused local const — was tree-shaken away and never reached dist, and is reported here as void rather than as a dead instrument.

scripts/check-changeset-no-major.mjs: ✅ No changeset declares a 'major' bump. No skip-changeset label was applied; it is a phantom label in this repo and a labelled bypass was declined twice.

pnpm check:control-bytes: ✅ check-control-bytes: OK (scanned 6653 tracked text file(s); skipped 85 binary), plus a direct grep -naP control-byte scan of the edited file with a firing lit control.

All readings above were re-taken at the final head 4a134d170 with a clean working tree.

Out of scope, and not filed — needs the PM

The nested (aiInitialMessages as any) is now measured live, not merely suspected: it hides a genuine TS2322, because the aiInitialMessages builder at :545 constructs parts as Record<string, unknown>[], which is not a UIMessagePart union. The real fix is at the producer (build real UIMessagePart values), not a wider cast at the consumer — contract-first, AGENTS.md #0.1.

I could not file it as its own card: dedupe search was unavailable this session (MCP search_issues returned API rate limit already exceeded on two attempts; repo-scoped REST returns 403 GitHub access is not enabled for this session, an app-installation policy rather than a quota, while /rate_limit itself answers 200). Filing without a dedupe read is not allowed, and dropping the finding silently is not allowed either, so it goes to the PM in the report to file.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

…options object

`useChat({...} as any)` widened the whole options argument to `any`, switching
off checking of `transport`, `onError` and excess properties, and blocking
inference of `UI_MESSAGE`. Measured: with the nested `(aiInitialMessages as any)`
in place, the outer cast hides nothing — both type-check projects stay at exit 0
with zero diagnostics without it — and the resolved instantiation is unchanged,
`UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>` either way, because
`transport` is the only inference source and it is already pinned to that type.

This narrows the suppression rather than eliminating it: removing BOTH casts is
red (TS2322, `Record<string, unknown>` is not a `UIMessagePart`), so the nested
cast is the live one. A comment now records that, so the next reader does not
delete it blind or re-widen the call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…ng nothing

Empty frontmatter, the gate's documented exemption. Argued from measurement:
building @object-ui/plugin-chatbot from this branch and from the merge-base
produced a byte-identical dist across all 34 published artifacts, and that
comparison was proved live by a control mutation to an emitted string literal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions github-actions Bot added the plugin label Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3472.4 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-BqSg3AyZ.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.55KB 114.03KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 189.15KB 52.56KB
fields (index.js) 243.15KB 61.40KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.49KB 46.33KB
plugin-dashboard (index.js) 131.48KB 34.45KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.66KB 63.92KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 55.38KB 15.72KB
plugin-list (index.js) 113.38KB 27.73KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.46KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-justin
os-justin marked this pull request as ready for review September 7, 2026 22:23
@os-justin
os-justin added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit c7360d4 Sep 7, 2026
34 checks passed
@os-justin
os-justin deleted the claude/issue-8378-dead-usechat-cast branch September 7, 2026 22:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(plugin-chatbot): useObjectChat's second cast, useChat({...} as any), is DEAD — measured, removing it leaves zero diagnostics

2 participants